package network;

//~--- JDK imports ------------------------------------------------------------
import java.io.*;

import java.math.BigInteger;

import java.net.*;
import java.nio.ByteBuffer;

import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;

import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPrivateKeySpec;
import java.security.spec.RSAPublicKeySpec;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.crypto.KeyAgreement;
import javax.crypto.interfaces.DHPublicKey;

import javax.crypto.spec.DHParameterSpec;
import javax.crypto.spec.DHPublicKeySpec;
import org.bouncycastle.crypto.BlockCipher;
import org.bouncycastle.crypto.BufferedBlockCipher;
import org.bouncycastle.crypto.DataLengthException;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.engines.AESEngine;
import org.bouncycastle.crypto.modes.CBCBlockCipher;
import org.bouncycastle.crypto.modes.PaddedBlockCipher;
import org.bouncycastle.crypto.params.KeyParameter;

//La classe implementa il socket server
public class Server implements Runnable {

    private boolean stop = true;
    private PrivateKey RsaServerPrivateKey;
    private PublicKey RsaServerPublicKey, RsaClientPublicKey;
    private int port;
    private ServerSocket socket;
    private int state;    // 0: pronto per accettare connessioni
    private BigInteger p, g;
    private PrivateKey Ss;
    private PublicKey gSs;
    private PublicKey gSc;
    private byte[] session_key;

    public Server(int p) {
        this.port = p;
        this.state = 0;

        BufferedReader keyIn;
        BigInteger keyModulus;
        BigInteger keyExponent;
        KeyFactory fact;

        try {
            this.socket = new ServerSocket(this.port);

            // Rileggo la chiave
            keyIn = new BufferedReader(new FileReader("sPrivate.key"));
            keyModulus = new BigInteger(keyIn.readLine(), 16);
            keyExponent = new BigInteger(keyIn.readLine(), 16);
            keyIn.close();
            RSAPrivateKeySpec keyspec1 = new RSAPrivateKeySpec(keyModulus, keyExponent);
            fact = KeyFactory.getInstance("RSA");
            RsaServerPrivateKey = fact.generatePrivate(keyspec1);

            keyIn = new BufferedReader(new FileReader("sPublic.key"));
            keyModulus = new BigInteger(keyIn.readLine(), 16);
            keyExponent = new BigInteger(keyIn.readLine(), 16);
            keyIn.close();
            RSAPublicKeySpec keyspec2 = new RSAPublicKeySpec(keyModulus, keyExponent);
            fact = KeyFactory.getInstance("RSA");
            RsaServerPublicKey = fact.generatePublic(keyspec2);

            keyIn = new BufferedReader(new FileReader("cPublic.key"));
            keyModulus = new BigInteger(keyIn.readLine(), 16);
            keyExponent = new BigInteger(keyIn.readLine(), 16);
            keyIn.close();
            RSAPublicKeySpec keyspec3 = new RSAPublicKeySpec(keyModulus, keyExponent);
            fact = KeyFactory.getInstance("RSA");
            RsaClientPublicKey = fact.generatePublic(keyspec3);

        } catch (FileNotFoundException e) {
            System.out.println("File non trovato. Server terminato");
            System.exit(0);
        } catch (NumberFormatException e) {
            System.out.println("Chiave non letta in modo corretto. Server terminato");
            System.exit(0);
        } catch (NoSuchAlgorithmException e) {
            System.out.println("Algoritmo non supportato. Server terminato");
        } catch (InvalidKeySpecException e) {
            System.out.println("Spefiche RSA errate. Server terminato");
            System.exit(0);
        } catch (IOException e) {
            System.out.println("Inizializzazione socket fallita. Server Terminato");
            System.exit(0);
        }
    }

    public void stop() {
        this.stop = false;
    }

    @Override
    public void run() {
        Socket client_conn = null;
        ;
        try {
            client_conn = socket.accept();
            Network.writeConsole(0, "Connessione avvenuta. In attesa di autenticazione");
            while (stop) {
                TLV pack = TLV.fetchPackage(client_conn);
                //System.out.println("SERVER DICE: Pacchetto ricevuto con type:" + pack.getType());
                if (pack.getType() == TLV.SESSION_END) {
                    Network.writeConsole(1, "SERVER DICE: Ricervuto pacchetto di terminazione. Sessione Terminata");
                    socket.close();
                    stop();
                }
                dispatcher(client_conn, pack);
            }
        } catch (AbortConnection ex) {
            try {
                client_conn.close();
                socket.close();
                stop();
            } catch (IOException ex1) {
                Logger.getLogger(Server.class.getName()).log(Level.SEVERE, null, ex1);
            }
        } catch (IOException ex) {
        }
    }

    private void dispatcher(Socket client, TLV pack) throws IOException, AbortConnection {
        BufferedReader buff = new BufferedReader(new InputStreamReader(System.in));
        String responce = new String();
        switch (state) {
            case 0:    // Server pronto per autenticazione e inattesa dei pachetti contenenti p e g
                if (pack.getType() == TLV.AUTH_INIT_SEND_P) {
                    p = new BigInteger(pack.getValue());
                    Network.writeConsole(1, "SERVER DICE: Ricevuto parametro p");

                } else if (pack.getType() == TLV.AUTH_INIT_SEND_G) {
                    g = new BigInteger(pack.getValue());
                    Network.writeConsole(1, "SERVER DICE: Ricevuto parametro g");
                } else {
                    Network.writeConsole(1, "SERVER_ERRORE: parametri p e g errati. Connessione Terminata");
                    throw new AbortConnection();
                }

                if (g != null && p != null) {
                    state = 1;
                    dispatcher(null, null);
                }
                break;
            case 1: //Generazione del random Sc e della sua esponenziazione 
                DHParameterSpec dhparam = new DHParameterSpec(p, g);

                KeyPairGenerator generator = null;
                try {
                    generator = KeyPairGenerator.getInstance("DH");
                    generator.initialize(dhparam);
                } catch (NoSuchAlgorithmException ex) {
                } catch (InvalidAlgorithmParameterException ex) {
                }
                KeyPair keys = generator.genKeyPair();
                Ss = keys.getPrivate();
                gSs = keys.getPublic();
                //p=null; g=null; //Distruzione dei attributi;
                state = 2;
                break;

            case 2:
                if (pack.getType() == TLV.AUTH_START) {
                    Network.writeConsole(1, "SERVER DICE: esponenziazione del client ricevuta (gSc)");
                    try {
                        BigInteger y = new BigInteger(pack.getValue());

                        KeyFactory kf = KeyFactory.getInstance("DH");
                        DHPublicKeySpec a = new DHPublicKeySpec(y, p, g);
                        gSc = kf.generatePublic(a);


                        //Preparo la risposta al client
                        //Invio di g^Ss al client
                        byte[] data = ((DHPublicKey) gSs).getY().toByteArray();
                        TLV pack_responce = new TLV(TLV.AUTH_RESPONCE, data);

                        client.getOutputStream().write(pack_responce.generatePackage());
                        Network.writeConsole(1, "SERVER DICE: esponenziazione inviata al client (gSs)");

                        //Posso derivare la chiave di sessione
                        KeyAgreement ka = KeyAgreement.getInstance("DH");
                        ka.init(Ss);
                        ka.doPhase(gSc, true);
                        session_key = ka.generateSecret();
                        Network.writeConsole(1, "SERVER DICE: chiave di sessione calcolata");
                        Network.writeConsole(1, "skey:" + new BigInteger(session_key).toString(16));


                        //Seguo l'invio della firma di g^Sc||g^Ss cifrata con la chiave di session
                        BigInteger gSs_number = ((DHPublicKey) gSs).getY();
                        BigInteger gSc_number = ((DHPublicKey) gSc).getY();
                        ByteBuffer proof = ByteBuffer.wrap(new byte[gSc_number.toByteArray().length + gSs_number.toByteArray().length]);
                        proof.put(gSc_number.toByteArray());
                        proof.put(gSs_number.toByteArray());

                        //Firmo con RSA il pacchetto
                        Signature sign = Signature.getInstance("SHA1withRSA");
                        sign.initSign(RsaServerPrivateKey);
                        sign.update(proof.array());
                        byte[] firma = sign.sign();

                        Network.writeConsole(0, "Vuoi inviare la firma del server corretta?(s/n)");
                        responce = buff.readLine();
                        if (responce.equals("n")) {
                            firma[0] = 15;
                        }

                        //Cifro la firma con l'algoritmo simmetrico di sessione
                        BlockCipher engine = new AESEngine();
                        BufferedBlockCipher cipher = new PaddedBlockCipher(new CBCBlockCipher(engine));
                        cipher.init(true, new KeyParameter(session_key)); //True sta per cifratura, false per decifratura
                        byte[] encrypted = new byte[cipher.getOutputSize(firma.length)];
                        int outputLen = cipher.processBytes(firma, 0, firma.length, encrypted, 0);
                        cipher.doFinal(encrypted, outputLen);

                        pack = new TLV(TLV.AUTH_RESPONCE_SIGN, encrypted);
                        client.getOutputStream().write(pack.generatePackage());
                        Network.writeConsole(1, "SERVER DICE: prova di identità inviata al client");


                        //Resto in attesa dell'autenticazione del client
                        Network.writeConsole(1, "SERVER DICE: in attesa di verificare l'identità del client");
                        pack = TLV.fetchPackage(client);
                        if (pack.getType() == TLV.AUTH_RESPONCE_SIGN) {
                            Network.writeConsole(1, "SERVER DICE: prova di identità del client ricevuta");
                            //Decripto il contenuto
                            engine = new AESEngine();
                            cipher = new PaddedBlockCipher(new CBCBlockCipher(engine));
                            cipher.init(false, new KeyParameter(session_key)); //True sta per cifratura, false per decifratura
                            byte[] pre_decrypted = new byte[cipher.getOutputSize(pack.getValue().length)];
                            outputLen = cipher.processBytes(pack.getValue(), 0, pack.getValue().length, pre_decrypted, 0);
                            cipher.doFinal(pre_decrypted, outputLen);

                            //Rimuovo lo zero padding
                            byte[] decrypted = new byte[outputLen];
                            ByteBuffer.wrap(pre_decrypted).get(decrypted, 0, outputLen);
                            Network.writeConsole(1, "SERVER DICE: prova di identità del client decriptata");

                            //Ricostruisco al challenge attesa
                            proof = ByteBuffer.wrap(new byte[gSs_number.toByteArray().length + gSc_number.toByteArray().length]);
                            proof.put(gSc_number.toByteArray());
                            proof.put(gSs_number.toByteArray());

                            //Verifico al firma
                            sign.initVerify(RsaClientPublicKey);
                            sign.update(proof.array());
                            boolean verify = sign.verify(decrypted);
                            if (verify) {
                                Network.writeConsole(4, "SERVER DICE: firma verificata! Client autenticato!!!!!");
                                //AUTENTICAZIONE TERMINATA
                                p = null;
                                g = null;
                                Ss = null;
                                gSs = null;
                                gSc = null;
                                state = 3;
                            } else {
                                Network.writeConsole(3, "SERVER_ERRORE: Firma non valida. Connessione Terminata");

                                throw new AbortConnection();
                            }
                        } else {
                            if (pack == null || pack.getType() == 0) {
                                Network.writeConsole(3, "SERVER_ERRORE: Il client ha chiuso la connessione. Connessione Terminata");

                            } else {
                                Network.writeConsole(3, "SERVER_ERRORE: Ricevuto un messaggio non previsto, si attendeva prova di identità. Connessione Terminata");

                            }
                            throw new AbortConnection();
                        }
                    } catch (DataLengthException ex) {
                        Logger.getLogger(Server.class.getName()).log(Level.SEVERE, null, ex);
                    } catch (IllegalStateException ex) {
                        Logger.getLogger(Server.class.getName()).log(Level.SEVERE, null, ex);
                    } catch (InvalidCipherTextException ex) {
                        Network.writeConsole(3, "SERVER_ERRORE: Chiave di sessione non valida. Connessione Terminata");

                        throw new AbortConnection();
                    } catch (SignatureException ex) {
                        Logger.getLogger(Server.class.getName()).log(Level.SEVERE, null, ex);
                    } catch (InvalidKeyException ex) {
                        Logger.getLogger(Server.class.getName()).log(Level.SEVERE, null, ex);
                    } catch (InvalidKeySpecException ex) {
                        Logger.getLogger(Server.class.getName()).log(Level.SEVERE, null, ex);
                    } catch (NoSuchAlgorithmException ex) {
                        Logger.getLogger(Server.class.getName()).log(Level.SEVERE, null, ex);
                    } catch (IOException ex) {
                        Logger.getLogger(Server.class.getName()).log(Level.SEVERE, null, ex);
                    }
                } else {
                    Network.writeConsole(3, "SERVER_ERRORE: Ricevuto un messaggio non previsto, si attendeva una esponenziazione. Connessione Terminata");

                    throw new AbortConnection();
                }
                break;
        }
    }
}
